﻿using System;
using System.IO;
using System.IO.Compression;
using System.Net;

namespace GerbilLib
{
    /// <summary>
    /// Downloads files from FTP or HTTP synchronously or asynchronously.
    /// </summary>
    public class FileDownloader
    {
        #region .   Public Properties   .
        /// <summary>
        /// Server Request State Object
        /// </summary>
        public WebRequestState ReqState { get; set; }
        /// <summary>
        /// Server Web Request Object
        /// </summary>
        private WebRequest Request { get; set; }
        /// <summary>
        /// Uri of the file being downloaded
        /// </summary>
        private Uri FileUri { get; set; }
        #endregion

        #region .   Constructor   .
        /// <summary>
        /// Creates a new FileDownloader object and initializes
        /// it with the given Uri. It can take both FTP or
        /// HTTP Uris.
        /// </summary>
        /// <param name="fileUri">Uri of the file to download</param>
        public FileDownloader(Uri fileUri)
        {
            FileUri = fileUri;

            if (FileUri.Scheme == Uri.UriSchemeHttp) // This is an HTTP download
            {
                Request = (HttpWebRequest)HttpWebRequest.Create(FileUri);
                ReqState = new HttpWebRequestState(WebRequestState.DefaultBufferSize);
            }
            else if (FileUri.Scheme == Uri.UriSchemeFtp) // This is an FTP download
            {
                // Create request object
                Request = (FtpWebRequest)FtpWebRequest.Create(FileUri);
                // Set credentials
                Request.Credentials = new NetworkCredential("anonymous", "mediagerbil@needforgeek.com");
                ReqState = new FtpWebRequestState(WebRequestState.DefaultBufferSize);
            }

            ReqState.Request = Request;
        }
        #endregion

        #region .   Public Methods   .
        /// <summary>
        /// Initiates a synchronous download of the file.
        /// </summary>
        /// <param name="destination">Path to where the file should be saved.</param>
        public void Download(string destination)
        {
            FileStream fs = null;
            WebResponse response = null;
            try
            {
                fs = new FileStream(destination, FileMode.Create, FileAccess.Write);
                response = Request.GetResponse();
            }
            catch (Exception ex)
            {
                Logger.LogMessage(string.Format("FileDownloader: An exception occurred when trying to download a file to destination {0}: {1}", destination, ex.Message), LoggingLevel.Error);
                return;
            }

            Stream stream = response.GetResponseStream();

            // Allocate 4kB buffer
            byte[] bytes = new byte[4096];
            int bytesRead = 0;

            // Read from the stream and write to the destination file
            bytesRead = stream.Read(bytes, 0, bytes.Length);
            while (bytesRead > 0)
            {
                fs.Write(bytes, 0, bytesRead);
                bytesRead = stream.Read(bytes, 0, bytes.Length);
            }

            // We're done, cleanup
            Logger.LogMessage(string.Format("FileDownloader downloaded file {0} to destination {1}.", Path.GetFileName(response.ResponseUri.AbsolutePath), destination), LoggingLevel.Info);
            fs.Close();
            stream.Close();
        }

        /// <summary>
        /// Initiates an asynchronous download of the file.
        /// </summary>
        public void AsyncDownload()
        {
            if (FileUri.Scheme == Uri.UriSchemeFtp) // This is an FTP download
            {
                // Set FTP-specific stuff
                ((FtpWebRequest)Request).KeepAlive = false;
                ((FtpWebRequest)Request).Method = WebRequestMethods.Ftp.DownloadFile;
            }

            try
            {
                // Start the asynchronous request.
                IAsyncResult result = (IAsyncResult)Request.BeginGetResponse(new AsyncCallback(ResponseCallback), ReqState);
            }
            catch (Exception ex)
            {
                Logger.LogMessage(string.Format("FileDownloader Exception while trying to download file: {0}", ex.Message), LoggingLevel.Error);
            }
        }

        /// <summary>
        /// Retrieves the filesize information from an FTP server
        /// using the FTP SIZE command or from a Web server by
        /// sending an HTTP request.
        /// Note that the SIZE command might not be supported by all
        /// FTP servers or when connecting through an HTTP proxy,
        /// in that case use the GetFileSizeFromDirectoryDetails
        /// method instead.
        /// <returns>The size of the file in bytes</returns>
        public long GetFileSize()
        {
            if (FileUri.Scheme == Uri.UriSchemeFtp)
            {
                // Reduce timeout to 15 seconds
                Request.Timeout = 15000;
                // Set FTP-specific stuff
                ((FtpWebRequest)Request).KeepAlive = false;
                ((FtpWebRequest)Request).Method = WebRequestMethods.Ftp.GetFileSize;
            }

            try
            {
                WebResponse response = null;
                // Start the synchronous file size request.
                if (FileUri.Scheme == Uri.UriSchemeHttp) response = (HttpWebResponse)Request.GetResponse();
                else if (FileUri.Scheme == Uri.UriSchemeFtp) response = (FtpWebResponse)Request.GetResponse();

                if (response != null) // Return the file size in bytes
                {
                    ReqState.TotalBytes = response.ContentLength;
                    return ReqState.TotalBytes;
                }
                else return 0;
            }
            catch (Exception ex)
            {
                Logger.LogMessage(string.Format("FileDownloader Exception while trying to get filesize: {0}", ex.Message), LoggingLevel.Error);
                return 0;
            }
        }

        /// <summary>
        /// Retrieves the directory details from an FTP server to
        /// get additional information such as date or size of a
        /// file. This method will work through an HTTP proxy.
        /// Note that this method will return only a single line if
        /// the Uri passed into FileDownloader points to a file and
        /// not a directory.
        /// </summary>
        /// <returns>String with directory details.</returns>
        public string GetDirectoryDetails()
        {
            if (FileUri.Scheme == Uri.UriSchemeFtp)
            {
                // Reduce timeout to 15 seconds
                Request.Timeout = 15000;
                // Set FTP-specific stuff
                ((FtpWebRequest)Request).KeepAlive = false;
                ((FtpWebRequest)Request).Method = WebRequestMethods.Ftp.ListDirectoryDetails;
            }

            try
            {
                WebResponse response = null;
                // Start the synchronous request.
                if (FileUri.Scheme == Uri.UriSchemeFtp) response = (FtpWebResponse)Request.GetResponse();

                if (response != null)
                {
                    // Read directory details response synchronously since this should not
                    // be a whole lot of data.
                    StreamReader reader = new StreamReader(response.GetResponseStream());
                    // TODO: This only returns the first line, will need to be changed to support directory Uris.
                    string line = reader.ReadLine();
                    if (line != null)
                    {
                        // Example: "-rw-r--r--   1 ZEDAT    FU-Berlin 17479771 Oct 22 19:22 movies.list.gz"
                        return line;
                    }
                }
                
                return string.Empty;
            }
            catch (Exception ex)
            {
                Logger.LogMessage(string.Format("FileDownloader Exception while trying to get filesize: {0}", ex.Message), LoggingLevel.Error);
                return string.Empty;
            }
        }

        /// <summary>
        /// Retrieves the filesize information from an FTP server
        /// by using the directory details, which will also work
        /// through an HTTP proxy server.
        /// Note that since the directory detail output varies
        /// significantly depending on the FTP server that returns
        /// it, the size is determined right now by taking the
        /// largest number in the string. This might cause the
        /// wrong value to be returned for very small files.
        /// </summary>
        /// <returns>Size of the file in bytes</returns>
        public long GetFileSizeFromDirectoryDetails()
        {
            long size = 0;
            string details = GetDirectoryDetails();
            if (string.IsNullOrEmpty(details.Trim())) return 0;

            // Assume the parts are delimited by spaces
            string[] parts = details.Split(' ');
            if (parts != null && parts.Length > 0)
                foreach (string part in parts) // Get the largest number
                {
                    long newsize = 0;
                    long.TryParse(part, out newsize);
                    if (newsize > size) size = newsize;
                }
            // Persist size in request state object
            ReqState.TotalBytes = size;
            
            return size;
        }

        /// <summary>
        /// Main response callback, invoked once we have first Response packet from
        /// server. This is where we initiate the actual file transfer, reading from
        /// a stream.
        /// </summary>
        /// <param name="asyncResult">Asynchronous Result Object</param>
        public static void ResponseCallback(IAsyncResult asyncResult)
        {
            try
            {
                // Will be either HttpWebRequestState or FtpWebRequestState
                WebRequestState reqState = ((WebRequestState)(asyncResult.AsyncState));
                WebRequest req = reqState.Request;
                string statusDescription = string.Empty;
                long contentLength = 0;

                // HTTP
                if (reqState.Request.RequestUri.Scheme == Uri.UriSchemeHttp)
                {
                    HttpWebResponse resp = ((HttpWebResponse)(req.EndGetResponse(asyncResult)));
                    reqState.Response = resp;
                    statusDescription = resp.StatusDescription;
                    reqState.TotalBytes = reqState.Response.ContentLength;
                    contentLength = reqState.Response.ContentLength; // # bytes
                }
                // FTP part 1 - response to GetFileSize command
                else if ((reqState.Request.RequestUri.Scheme == Uri.UriSchemeFtp) &&
                (reqState.Request.Method == WebRequestMethods.Ftp.GetFileSize))
                {
                    // First FTP command was GetFileSize, so this 1st response is the size of
                    // the file.
                    FtpWebResponse resp = ((FtpWebResponse)(req.EndGetResponse(asyncResult)));
                    statusDescription = resp.StatusDescription;
                    reqState.TotalBytes = resp.ContentLength;
                    contentLength = resp.ContentLength; // # bytes
                }
                // FTP part 2 - response to DownloadFile command
                else if ((reqState.Request.RequestUri.Scheme == Uri.UriSchemeFtp) &&
                (reqState.Request.Method == WebRequestMethods.Ftp.DownloadFile))
                {
                    FtpWebResponse resp = ((FtpWebResponse)(req.EndGetResponse(asyncResult)));
                    reqState.Response = resp;
                }

                // Get this info back to the GUI -- max # bytes, so we can do progress bar
                if (statusDescription != "")
                    reqState.ResponseInfoCallback(statusDescription, contentLength);

                // FTP part 1 done, need to kick off 2nd FTP request to get the actual file
                if ((reqState.Request.RequestUri.Scheme == Uri.UriSchemeFtp) && (reqState.Request.Method == WebRequestMethods.Ftp.GetFileSize))
                {
                    // Note: Need to create a new FtpWebRequest, because we're not allowed to change .Method after
                    // we've already submitted the earlier request. I.e. FtpWebRequest not recyclable.
                    // So create a new request, moving everything we need over to it.
                    FtpWebRequest req2 = (FtpWebRequest)FtpWebRequest.Create(reqState.Request.RequestUri);
                    req2.Credentials = req.Credentials;
                    req2.UseBinary = true;
                    req2.KeepAlive = true;
                    req2.Method = WebRequestMethods.Ftp.DownloadFile;

                    reqState.Request = req2;
                    reqState.Request.Method = WebRequestMethods.Ftp.DownloadFile;

                    // Start the asynchronous request, which will call back into this same method
                    IAsyncResult result =
                    (IAsyncResult)req2.BeginGetResponse(new AsyncCallback(ResponseCallback), reqState);
                }
                else // HTTP or FTP part 2 -- we're ready for the actual file download
                {
                    Logger.LogMessage(string.Format("FileDownloader beginning download of {0}.", reqState.Request.RequestUri));
                    reqState.TransferStart = DateTime.Now;

                    // Set up a stream, for reading response data into it
                    Stream responseStream = reqState.Response.GetResponseStream();
                    reqState.StreamResponse = responseStream;

                    // Set up a stream for writing data into
                    reqState.FileStream = new FileStream(Path.GetFileName(reqState.Request.RequestUri.AbsolutePath), FileMode.Create);

                    // Begin reading contents of the response data
                    IAsyncResult ar = responseStream.BeginRead(reqState.BufferRead, 0, WebRequestState.DefaultBufferSize, new AsyncCallback(ReadCallback), reqState);
                }

                return;
            }
            catch (Exception ex)
            {
                Logger.LogMessage(string.Format("FileDownloader Exception in ResponseCallback(): {0}", ex.Message), LoggingLevel.Debug);
            }
        }

        /// <summary>
        /// Takes a GZip compressed file and decompresses it using the
        /// same filename without the .gz extension.
        /// Note that this method does not check whether the input file
        /// is actually compressed.
        /// </summary>
        /// <param name="filename">Path to the file being uncompressed</param>
        /// <param name="deleteOriginalFile">If true the original file will be deleted
        /// after decompression</param>
        public static void DecompressFile(string filename, bool deleteOriginalFile = true)
        {
            // Open the input file
            using (FileStream reader = new FileStream(filename, FileMode.Open, FileAccess.Read))
            {
                string newFilename = Path.GetFileNameWithoutExtension(filename);

                // Create GZip and destination file streams
                GZipStream uncompressor = new GZipStream(reader, CompressionMode.Decompress);
                FileStream writer = new FileStream(newFilename, FileMode.Create, FileAccess.Write);
                // Allocate 4kB buffer
                byte[] bytes = new byte[4096];
                int bytesRead = 0;

                // Read from the GZip stream and write to the destination file
                bytesRead = uncompressor.Read(bytes, 0, bytes.Length);
                while (bytesRead > 0)
                {
                    writer.Write(bytes, 0, bytesRead);
                    bytesRead = uncompressor.Read(bytes, 0, bytes.Length);
                }

                // We're done, cleanup
                Logger.LogMessage(string.Format("FileDownloader Uncompressed file {0} into destination file {1}.", filename, newFilename), LoggingLevel.Info);
                writer.Close();
                uncompressor.Close();
                reader.Close();
            }

            if (deleteOriginalFile) File.Delete(filename);
        }
        #endregion

        #region .   Private Methods   .
        /// <summary>
        /// Main callback invoked in response to the Stream.BeginRead method, when we have some data.
        /// </summary>
        /// <param name="asyncResult">Asynchronous Result Object</param>
        private static void ReadCallback(IAsyncResult asyncResult)
        {
            try
            {
                // Will be either HttpWebRequestState or FtpWebRequestState
                WebRequestState reqState = ((WebRequestState)(asyncResult.AsyncState));

                Stream responseStream = reqState.StreamResponse;

                // Get results of read operation
                int bytesRead = responseStream.EndRead(asyncResult);

                // Got some data, need to read more
                if (bytesRead > 0)
                {
                    // Write to filestream
                    reqState.FileStream.Write(reqState.BufferRead, 0, bytesRead);

                    // Report some progress, including total # bytes read, % complete, and transfer rate
                    reqState.BytesRead += bytesRead;
                    double percentComplete = 0f;
                    if (reqState.TotalBytes != 0) percentComplete = ((double)reqState.BytesRead / (double)reqState.TotalBytes) * 100.0f;

                    // Note: bytesRead/totalMS is in bytes/ms. Convert to kb/sec.
                    TimeSpan totalTime = DateTime.Now - reqState.TransferStart;
                    double kbPerSec = (reqState.BytesRead * 1000.0f) / (totalTime.TotalMilliseconds * 1024.0f);

                    reqState.ProgressCallback(reqState.BytesRead, percentComplete, kbPerSec);

                    // Kick off another read
                    IAsyncResult ar = responseStream.BeginRead(reqState.BufferRead, 0, WebRequestState.DefaultBufferSize, new AsyncCallback(ReadCallback), reqState);
                    return;
                }
                else // EndRead returned 0, so no more data to be read, cleanup
                {
                    Logger.LogMessage(string.Format("FileDownloader completed downloading {0}.", reqState.Request.RequestUri));
                    responseStream.Close();
                    reqState.Response.Close();
                    reqState.FileStream.Close();
                    reqState.DoneCallback();
                }
            }
            catch (Exception ex)
            {
                Logger.LogMessage(string.Format("FileDownloader Exception in ReadCallback(): {0}", ex.Message), LoggingLevel.Debug);
                throw ex;
            }
        }
        #endregion
    }
}
